/**********************************************************************************
  Arduino函数说明：
   1.在arduino中，setup函数和loop函数是最主要的两个函数，是每个arduino程序基本都要有的函数。
   2.setup函数是程序运行时的入口，相当于main函数，setup函数只执行一次。
   3.loop函数会在setup执行完之后自动执行，loop函数会一直循环执行。
   4.本文件中其他的函数，如setup_wifi、callback、reconnect函数，都是自己编写的函数。
  其它说明：
   ESP8266承担着与ONENET的MQTT协议数据交互，以及与Arduino的串口通信功能。
   其中ESP8266与ONENET的通信是通过subscribe和publish函数实现的，
   ESP8266与Arduino通信是通过与串口有关的函数实现的，如Serial.print。
  特别说明：
   "tock1"主题是用来向MQTT服务器发布温湿度的主题，该主题是由ESP8266发布至ONENET，并由APP订阅以显示温湿度数据
   "inTpic1"主题是用来接收喂食时间的主题，该主题由APP发布数据至ONTNET，并由ESP8266订阅以获取喂食时间
 **********************************************************************************/
#include <ESP8266WiFi.h>          //这个库的参考文档：https://arduino-esp8266.readthedocs.io/en/latest/
#include <PubSubClient.h>         //这个库的参考文档：https://pubsubclient.knolleary.net/api

/*以下是你的WiFi配置*/
const char* ssid = "TEST";                    //连接的路由器的名字
const char* password = "fxwfxwfxw";           //连接的路由器的密码

/*以下是与ONENET相关的配置*/
const char* mqtt_server =  "183.230.40.39";   //ONENET服务器的地址
#define onenet_productId   "425032"           //产品ID
#define onenet_deviceId    "716266684"        //设备ID
#define onenet_apiKey      "EkWcG8yVIDi0ds24MXwU4gIBz9Q=" //API_KEY

WiFiClient espClient;
PubSubClient client(espClient);
int status = WL_IDLE_STATUS;
long lastMsg = 0;                             //存放时间的变量 
char msg[50];                                 //存放要发的数据
int i=0;

/**********************************************************************************
  setup_wifi函数功能：
    尝试连接WiFi，直到连接成功
 **********************************************************************************/
void setup_wifi() {                           //自动接入网络
  delay(10);                                  //延时10毫秒
  WiFi.begin(ssid, password);                 //开始连接WiFi
  while (WiFi.status() != WL_CONNECTED) {     //等待WiFi连接成功
    delay(500);                               //连接不成功的话，就等待500毫秒后重新尝试连接
  }
}

/**********************************************************************************
  callback函数功能：
    对订阅主题所接收到的消息进行处理，将接收到的消息内容通过串口打印出来
    注意：这里的串口打印实际上是esp8266向arduino发送数据
  其它说明：
    在MQTT协议中，topic表示主题，payload表示消息的内容
    在<PubSubClient.h>这个库中，如果使用客户端订阅主题，那么回调函数是必须要写的
    手册中推荐的回调函数写法是：
    void callback(const char[] topic, byte* payload, unsigned int length)
    其中，topic表示主题，payload表示消息的内容，length表示消息的有效长度
 **********************************************************************************/
void callback(char* topic, byte* payload, unsigned int length) {  //用于接收服务器接收的数据

  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);//串口打印出接收到的数据让你看看
  }
  Serial.println();//换行
}

/**********************************************************************************
  reconnect函数功能：
   1. 连接ONENET的MQTT服务
   2. 向ONENET的MQTT订阅以"inTpic1"为主题的消息
 **********************************************************************************/
void reconnect() {      //等待，直到连接上服务器
  while (!client.connected()) {   //如果没有连接到ONENET，就进入while循环
    /* client.connect用来尝试连接ONENET服务器，该函数的各项参数如下：
       boolean connect (clientID, [username, password], [willTopic, willQoS, willRetain, willMessage], [cleanSession])
       其中，clientID表示客户端ID。这里填写的是你的设备ID，这是ONENET要求的，具体请看https://open.iot.10086.cn/doc/mqtt/book/example/connect.html
            username和password是可选项，但ONENET要求填写以验证用户身份，它的值分别是你的产品ID和APIKEY
       connected函数参考官方手册：https://pubsubclient.knolleary.net/api#connect
    */ 
    if (client.connect(onenet_deviceId,onenet_productId,onenet_apiKey)) {   //如果连接成功，则返回true
      /*subscribe函数用来订阅指定主题的消息，该函数参数如下：
        boolean subscribe (topic, [qos])
        其中，topic表示要订阅的主题，这里你订阅了"inTpic1"。由于你订阅了"inTpic1"主题，所以当有客户端(手机)向服务器的"inTpic1"主题发送消息时，你就会收到相应的消息内容。
             qos表示服务质量，可选项
        subscribe函数参考手册：https://pubsubclient.knolleary.net/api#subscribe
      */
      client.subscribe("inTpic1");  //接收外来的数据时的inTpic1
    } else {          //如果连接失败，则进入else
      /*state函数用来返回客户端的连接状态，如果与ONENET连接失败，则可用于获取有关失败的更多信息。其返回值及表示的状态信息如下，具体请看手册：
          -1：MQTT_DISCONNECTED -客户端断开连接
          0：MQTT_CONNECTED -客户端已连接
        state函数参考手册：https://pubsubclient.knolleary.net/api#state
        这里使用Serial.print串口打印出连接状态，用来调试
      */
      Serial.print(client.state()); //重新连接
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

/**********************************************************************************
  setup函数功能：
   1. 初始化串口波特率为9600，注意：这里的串口是用来与arduino通信的
   2. 尝试连接WiFi
   3. 设置要连接的MQTT地址
   4. 设置订阅消息的回调函数
 **********************************************************************************/
void setup() {//初始化程序，只运行一遍
  char k=0;
  int commaPosition; 
  Serial.begin(9600);//设置串口波特率（与烧写用波特率不是一个概念）
  /*这里的波特率是esp8266与arduino通过串口通信时，所使用的波特率*/

  /*调用setup_wifi函数，用来连接WiFi*/
  setup_wifi();
  /*调用setServer函数，用来连接ONENET的MQTT服务器(Broker)。该函数的参数如下：
    PubSubClient* setServer (server, port)
    其中，server表示要连接的MQTT Broker的IP地址或域名
         port表示要连接的MQTT Broker的端口号
    该函数参考手册：https://pubsubclient.knolleary.net/api#setServer
  */
  client.setServer(mqtt_server, 6002);//1883为端口号
  
  /*调用setCallback函数，用来设置回调函数。该函数的参数如下：
    PubSubClient* setCallback (callback)
    其中，callback表示要设置的回调函数名
    如果使用客户端订阅主题，则必须在构造函数中提供回调函数
    这里的作用是：因为你的arduino作为客户端，订阅了"inTpic1"主题，所以必须要设置这个回调函数。
               当MQTT服务器接收到以"inTpic1"为主题的消息时，这个callback函数就会被调用，callback函数用来在串口输出消息内容
               因此，当有任何客户端(如手机)向"inTpic1"主题发送消息时，callback函数都会被调用，并在串口输出消息内容
    该函数参考：https://pubsubclient.knolleary.net/api#callback
  */
  client.setCallback(callback); 
}

/**********************************************************************************
 loop函数功能：
 1. 确保WiFi连接正常
 2. 确保能够连接至ONENET的MQTT服务器
 3. 每隔两秒，接收一次来自arduino的串口数据，并向ONENET发布
 **********************************************************************************/
void loop() {//主循环

  /************************** 检测WiFi是否已连接，确保WiFi正常连接 **********************/

  /*这里使用status函数获取WiFi连接状态，并将链接状态信息保存在status变量中
    我想这样是为了确保WiFi连接能够比较稳定，在WiFi断开的时候不会进行MQTT的相关操作*/
  status = WiFi.status();
  if ( status != WL_CONNECTED) {            /*如果此时没有连接上WiFi，则进入if*/
    
    while ( status != WL_CONNECTED) {       /*当没有连接上WiFi时，使用串口向arduino发送提示语句*/
      Serial.print("[loop()]Attempting to connect to WPA SSID: ");
      Serial.println(WIFI_AP);
      // 连接WiFi热点
      status = WiFi.begin(ssid, password);  /*一直尝试连接WiFi，直到连接WiFi成功*/
      delay(50);
    }
    Serial.println("[loop()]Connected to AP");
  }

  /**************************** WiFi连接成功，开始连接ONENET *************************/

  /*reconnect函数是自己封装的函数，用来连接ONENET的MQTT服务器，详细解释见该函数*/
  reconnect();//确保连上服务器，否则一直等待。

  /*loop函数用来维持arduino与onenet服务器的连接状态，该函数没有参数
    该函数的参考文档：https://pubsubclient.knolleary.net/api#loop
  */
  client.loop();//MUC接收数据的主循环函数。

  /**************************** ONENET连接成功，开始发布数据 *************************/

  /*millis函数返回自Arduino开发板开始运行当前程序以来的毫秒数。大约50天后，该数字将溢出(返回零)*/
  long now = millis();//记录当前时间
  /*这里用了一种相当于差值的方式，如果当前运行时间大于2000ms(2s)，就执行一次if语句，进行一次数据发布*/
  if (now - lastMsg > 2000) {//每隔2秒发一次信号
    /*将lastMsg赋值为now，相当于将差值清零，重新累计差值，直到下一次差值大于2000ms*/
    lastMsg = now;//刷新上一次发送数据的时间
    i=0;
    /*Serial.available函数用来检测能否从串口收到数据
      当从串口收到数据时未结束时，一直接收数据并将数据保存至msg中
      @@这里需要注意的是，串口接收的数据是由arduino发送过来的*/
    while(Serial.available()>0)
    {
       msg[i]=(char)Serial.read();
       i++;  
    }
    
    /*如果接收到的串口数据的开头字母为a，则调用publish函数，向ONENET发布数据。
      这里的串口数据是由arduino发过来的，'a'相当于是一个简单的串口通信协议*/
    if(msg[0]=='a')
    {
       /*publish函数用于向指定的主题发布数据，该函数的参数如下：
         boolean publish (topic, payload, [length], [retained])
         其中，topic表示要发布的主题；payload表示要发布的数据内容，这两个参数是必须有的。
         这里调用的publish函数，用来向MQTT服务器发布以"tock1"为主题的消息，其消息内容为从msg第二个字节开始的数据(因为第一个字节'a'用来判断该消息是否用于发布数据)
         参考手册：https://pubsubclient.knolleary.net/api#publish
       */
       client.publish("tock1", msg+1);

       /*memset函数用来将一段内存空间清空，这里是将msg的内存清空，避免多次数据传输造成混乱*/
       memset(msg,0,sizeof(msg));
    } 
  }
}
/************************************* End of file ******************************************/
